﻿#region MIT Licence
/*
 * Copyright (C) 2014 PdfCreative
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 
 * and associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom 
 * the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
 * AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#endregion

using System;
using System.Collections.Generic;
using System.Text;

namespace PdfCreative.Pdf.Graphics.Tables
{
    /// <summary>
    /// The TableWriter class draws tables onto pages. To use a TableWriter, call
    /// CreateTableWriter() on a Graphics instance and add columns and rows to
    /// build up a table on the current page.
    /// The TableWriter class raises several events throughout the table drawing
    /// process that can be used to customise the look and behaviour of the table.
    /// </summary>
    public class TableWriter
    {
        // Private fields
        // ==============
        #region Private fields
        private Rectangle _bounds;                      // A rectangle that stores the position and width of
                                                        // the table.

        private Rectangle _cell;                        // The bounds of the next cell.
        private List<Column> _columns;                  // The columns used to render the table.
        private StringFormat _defaultFormat;            // The default format for cell content.
        private int[] _extensions;                      // We keep track of cells in previous rows that extend down
                                                        // to subsequent rows, so that we know which cells to draw
                                                        // for each row, and which to skip.

        private Graphics _g;                            // The Graphics instance to write the table to.
        private int _pendingRows;                       // The number of rows that still need to be added
                                                        // before drawing can commence.

        private Queue<Row> _rowsToDraw;                 // The queue of outstanding rows yet to be drawn.
        private TableWriterState _state;                // Has a table been opened yet?
        private TableOptions _tableOptions;             // The table options.
        #endregion



        // Public events
        // =============
        #region Public events
        /// <summary>
        /// The HeaderPaint is raised before the first row is drawn on a page. It
        /// allows for the custom drawing of a table header.
        /// </summary>
        public event EventHandler HeaderPaint;

        /// <summary>
        /// The CellPaint event is raised when a cell is drawn. When listening for the
        /// CellPaint event, the listener must paint the cell content or else no operation
        /// will be performed, and the cell will be invisible.
        /// If there are no listeners to this event, the TableWriter instance will
        /// paint the default cell content.
        /// </summary>
        public event EventHandler CellPaint;

        /// <summary>
        /// The BorderPaint event is raised when a cell's borders are drawn. When listening for
        /// the BorderPaint event, the listener must paint the cell borders or else no operation
        /// will be performed, and the cell will be invisible.
        /// If there are no listeners to this event, the TableWriter instance will draw the
        /// default borders for the cell.
        /// </summary>
        public event EventHandler BorderPaint;

        /// <summary>
        /// The HeaderPaint is raised after the last row is drawn on a page. It
        /// allows for the custom drawing of a table header.
        /// </summary>
        public event EventHandler FooterPaint;
        #endregion



        // Public methods
        // ==============
        #region Public methods
        /// <summary>
        /// Closes the table. No more rows can be written to the table after it has been closed.
        /// </summary>
        /// <exception cref="InvalidOperationException">A table has not been opened, so it cannot be closed.</exception>
        public void CloseTable()
        {
            if (TableWriterState.Closed==_state)
                throw new InvalidOperationException("A table has not been opened, so it cannot be closed.");

            FinaliseTable();

            // The table is now closed.
            _state = TableWriterState.Closed;
        }

        /// <summary>
        /// Opens a table. The specified columns will form the basis of the table's measurements.
        /// </summary>
        /// <param name="left">The left position of the table.</param>
        /// <param name="top">The top position of the table.</param>
        /// <param name="columns">The columns.</param>
        /// <exception cref="ArgumentNullException">The list of columns cannot be a Null reference.</exception>
        /// <example cref="InvalidOperationException">A table had already been opened.</example>
        public void OpenTable(float left, float top, IEnumerable<Column> columns)
        {
            OpenTable(left, top, columns, new TableOptions());
        }

        /// <summary>
        /// Opens a table. The specified columns will form the basis of the table's measurements.
        /// The table options will control the padding and spacing of table cells.
        /// </summary>
        /// <param name="left">The left position of the table.</param>
        /// <param name="top">The top position of the table.</param>
        /// <param name="columns">The columns.</param>
        /// <param name="options">The table options.</param>
        /// <exception cref="ArgumentNullException">The list of columns or table options cannot be a Null reference.</exception>
        /// <example cref="InvalidOperationException">A table had already been opened.</example>
        public void OpenTable(float left, float top, IEnumerable<Column> columns, TableOptions options)
        {
            if (null == columns)
                throw new ArgumentNullException("columns");

            if (TableWriterState.Closed != _state)
                throw new InvalidOperationException("A table has already been opened.");

            if (null == options)
                throw new ArgumentNullException("options");

            // Update the state of the table writer.
            _bounds = new Rectangle(left, top, CalculateActualTableWidth(columns), 0);
            _columns = new List<Column>(columns);
            _extensions = new int[_columns.Count];
            _tableOptions = options;

            // The table has now been opened.
            _state = TableWriterState.Opened;

            // Start the table. This will allow listeners to include table headers.
            StartTable();
        }

        /// <summary>
        /// Writes a row to the table. The table must be opened first with an OpenTable() call.
        /// The row can not be a Null reference.
        /// </summary>
        /// <param name="row">The row instance.</param>
        /// <exception cref="ArgumentNullException">The row can not be a Null reference.</exception>
        /// <exception cref="InvalidOperationException">A table has not been opened.</exception>
        public void WriteRow(Row row)
        {
            if (TableWriterState.Closed == _state)
                throw new InvalidOperationException("A table has not been opened. Call OpenTable() before writing rows.");

            if (null == row)
                throw new ArgumentNullException("row");


            // Determine the number of rows that need to be written before they can be drawn.
            int span = 0; // GetLargestRowSpan(row.Cells);

            if (0 == _pendingRows)
            {
                // If no rows are pending, and the cells in the current row span just one row,
                // then we draw the row.
                if (1 == span) DrawRow(row);
                else
                {
                    // If no rows are pending, and the current row contains cells that span multiple
                    //rows then we store the row and remember how many more rows we need to process.
                    _rowsToDraw.Enqueue(row);
                    _pendingRows = span - 1;
                }
            }
            else
            {
                // There are pending rows.
                // This means we add the current row to the queue of rows to process.
                _rowsToDraw.Enqueue(row);

                // The number of pending rows goes down one. If the current row spans multiple
                // rows then we need to adjust for that.
                _pendingRows = _pendingRows - 1 + (span - 1);

                // If no more rows are pending now, we can process them all.
                if (0 == _pendingRows)
                {
                    DrawRows();
                }
            }
        }

        /// <summary>
        /// Writes a full table to the page. This method opens the table, sets up its columns if required,
        /// and writes all of its rows before closing it. Use WriteTable where auto-adjusted columns are required,
        /// as this method calculates optimal column widths prior to drawing the table contents.
        /// This method will throw an exception if a table has already been opened with a call to OpenTable().
        /// </summary>
        /// <param name="table">The table instance.</param>
        /// <param name="left">The left position of the table.</param>
        /// <param name="top">The top position of the table.</param>
        /// <exception cref="ArgumentNullException">The table can not be a Null reference.</exception>
        /// <exception cref="InvalidOperationException">A table has already been opened.</exception>
        public void WriteTable(Table table, float left, float top)
        {
            if (TableWriterState.Closed != _state)
                throw new InvalidOperationException("A table has already been opened.");

            if (null == table)
                throw new ArgumentNullException("table");

            try
            {
                // Create the columns for this table. Columns with no set width will have an optimal
                // width calculated for them.
                List<Column> columns = CreateColumns(table);

                // Using the columns, the table is opened.
                OpenTable(left, top, columns, table.Options);

                // Write each row in the table
                foreach (Row row in table.Rows)
                    WriteRow(row);

            }
            finally
            {
                // And finally, close the table.
                // The table needs to be closed at the end of the operation,
                // regardless of success or failure.
                if(TableWriterState.Closed!=_state) CloseTable();
            }
        }
        #endregion



        // Private implementation
        // ======================
        #region Private implementation
        /// <summary>
        /// Returns the actual width of the table, based on its calculated columns.
        /// </summary>
        /// <param name="columns">The columns.</param>
        /// <returns>The width of the table.</returns>
        private float CalculateActualTableWidth(IEnumerable<Column> columns)
        {
            float width = 0;
            
            foreach (Column column in columns) width += column.Width.Value;
            
            return width;
        }

        /// <summary>
        /// Calculates the space available to variable-width columns. This
        /// is the width of the table, minus the total width of its fixed
        /// columns.
        /// If the table does not have a set width, then the available space
        /// is based on the width of the page instead.
        /// </summary>
        /// <param name="table">The table</param>
        /// <returns>The available space</returns>
        private float CalculateAvailableSpace(Table table)
        {
            float available = table.Width.GetValueOrDefault(_g.Bounds.Width);

            foreach (Column column in table.Columns)
                if (column.Width.HasValue) 
                    available -= column.Width.Value;

            return available;
        }

        /// <summary>
        /// Calculate the maximum width of a column.
        /// This is the width of the widest cell in that column.
        /// </summary>
        /// <param name="table">The table.</param>
        /// <param name="columnIndex">The index of the column.</param>
        /// <returns>The column's maximum width.</returns>
        private float CalculateMaximumWidth(Table table, int columnIndex)
        {
            float result = 0f;

            // Go through the rows, measure the width of each cell's content as if it had no boundaries;
            // The widest cell's width will be returned.
            foreach (Row row in table.Rows)
            {
                Cell cell = row.Cells[columnIndex];
                result = Math.Max(result, _g.MeasureStyledText(cell.Content, cell.Format ?? _defaultFormat).Width);
            }

            // Include the padding.
            float padding = table.Options.Padding * 2f;
            return result + padding;
        }

        /// <summary>
        /// Calculates the maximum width for each variable-width column in the table.
        /// </summary>
        /// <param name="table">The table</param>
        /// <param name="variables">The variable cokumns</param>
        /// <returns>A dictionary that stores the maximum width for each variable column.</returns>
        private Dictionary<Column, float> CalculateMaximumWidths(Table table, IEnumerable<Column> variables)
        {
            Dictionary<Column, float> result = new Dictionary<Column, float>();

            // Go through each of the table's columns. If a column has a variable width then
            // find the maximum width of each cell in the column; that will be the column's
            // maximum width.
            IEnumerator<Column> iter = variables.GetEnumerator();
            int columnIndex = 0;
            foreach (Column column in table.Columns)
            {
                if (false == column.Width.HasValue)
                {
                    if (true == iter.MoveNext())
                    {
                        result[iter.Current] = CalculateMaximumWidth(table, columnIndex);
                    }
                }

                ++columnIndex; // Next column
            }
            return result;
        }

        /// <summary>
        /// Calculates the actual height of a row, based on the row's set height or the
        /// content of its cells.
        /// </summary>
        /// <param name="row">The row.</param>
        /// <returns>The actual height of the row.</returns>
        private float CalculateRowHeight(Row row)
        {
            float height = 0f;
            
            if(row.Height.HasValue)
                height = row.Height.Value;
            else
            {
                // Calculate the height of each cell in the row - the space required to fit
                // the cell content into a bounding box whose width is defined by the
                // corresponding column.
                for (int c = 0; c < row.Cells.Count; c++)
                {
                    Cell cell = row.Cells[c];
                    
                    height = Math.Max(height, _g.MeasureStyledText(
                        cell.Content, _columns[c].Width.Value, cell.Format ?? _defaultFormat).Height);
                }

                // Include the padding.
                float padding = _tableOptions.Padding * 2f;
                height += padding;
            }

            return height;
        }

        /// <summary>
        /// Collects the columns of a table into two lists - one for all columns, and
        /// one for only the variable-width columns. It is used by the
        /// CreateColumns() method to keep track of which columns need their optimal
        /// width calculated.
        /// </summary>
        /// <param name="table">The table.</param>
        /// <param name="columns">The list of the table's columns.</param>
        /// <param name="variables">The variable-width columns.</param>
        private void CollectColumns(Table table, List<Column> columns, List<Column> variables)
        {
            // For each column, check if it has a fixed width. If so,
            // add it to the list. If not, create a new column and add that
            // to the list, and to the 'variables' list.
            foreach (Column column in table.Columns)
            {
                if (true == column.Width.HasValue)
                    columns.Add(column);
                else
                {
                    Column surrogate = new Column(0);
                    variables.Add(surrogate);
                    columns.Add(surrogate);
                }
            }
        }

        /// <summary>
        /// Creates a list of columns for a table. Columns that have been specified with
        /// an exact width will be returned as-is; columns without a set width will
        /// have their optimum widths calculated, and a surrogate column is added to the
        /// list.
        /// </summary>
        /// <param name="table">The table.</param>
        /// <returns>The columns.</returns>
        private List<Column> CreateColumns(Table table)
        {
            System.Diagnostics.Debug.Assert(null != table);

            List<Column> columns = new List<Column>();

            // Collect the fixed and variable columns.
            List<Column> variables = new List<Column>();
            CollectColumns(table, columns, variables);
            
            // Calculate available space.
            float available = CalculateAvailableSpace(table);

            // Calculate the maximum width of each of the variable columns.
            Dictionary<Column, float> maximums = CalculateMaximumWidths(table, variables);

            // Distribute the available space until no more space is available
            // or all the columns have reached their maximum width.
            while (available > 0 && variables.Count > 0)
            {
                DistributeAvailableSpace(ref available, variables, maximums);
            }

            // And finally, return the list of columns.
            return columns;
        }

        /// <summary>
        /// Distributes the available space over the variable-width columns.
        /// Columns won't be given more space than they need (their maximum width).
        /// </summary>
        /// <param name="available">The available space.</param>
        /// <param name="variables">The variable columns.</param>
        /// <param name="maximums">The maximum widths for the columns.</param>
        private void DistributeAvailableSpace(
            ref float available, 
            List<Column> variables, 
            Dictionary<Column, float> maximums)
        {
            System.Diagnostics.Debug.Assert(variables.Count > 0);

            // Calculate how much space to share amongst the columns, and
            // add that space to each column, up to their maximums.
            float share = available / variables.Count;
            foreach (Column variable in variables)
            {
                float increase = Math.Min(share, maximums[variable] - variable.Width.Value);
                variable.Width += increase;
                available-= increase;

            }

            // Any columns that have reached their maximum are taken out of the
            // equation.
            variables.RemoveAll(c=>c.Width>=maximums[c]);
        }

        /// <summary>
        /// Finalises the table on this page. This allows listeners to include a table footer.
        /// </summary>
        private void FinaliseTable()
        {
            // If rows were written on this page then the footer should be drawn,
            // if there is one. Raise the FooterPaint event.
            if (TableWriterState.RowsWritten == _state)
                OnFooterPaint();
        }

        /// <summary>
        /// Draws a row.
        /// </summary>
        /// <param name="row">The row.</param>
        private void DrawRow(Row row)
        {
            // Determine the actual height of the row based on fixed row height or cell content.
            // Rows with a height of 0 don't need to be drawn at all.
            _cell.Height = CalculateRowHeight(row);

            if (_cell.Height > 0)
            {
                // Paint and draw the contents of each cell - or delegate the painting to a listener
                // if there is one.
                bool hasPaintListener = null != CellPaint;

                WriteCells(row.Cells, hasPaintListener);

                // return to the start of the row, and move one row down.
                _cell.Left = _bounds.Left;
                _cell.Move(0, _cell.Height);

                // A row has been written. This is only true if a row
                // was actually drawn. (i.e. it had a height >0)
                _state = TableWriterState.RowsWritten;
            }
        }

        /// <summary>
        /// Draws the rows whose output has been deferred.
        /// </summary>
        private void DrawRows()
        {
            // Paint and draw the contents of each cell - or delegate the painting to a listener
            // if there is one.
            bool hasPaintListener = null != CellPaint;

            // Go through the deferred rows and draw them all.
            while(_rowsToDraw.Count > 0)
            {
                Row row = _rowsToDraw.Dequeue();

                //TODO: replace with calculated value.
                if(row.Height.Value > 0)
                {
                    WriteCells(row.Cells, hasPaintListener);

                    // return to the start of the row, and move one row down.
                    _cell.Left = _bounds.Left;
                    _cell.Move(0, _cell.Height);

                    // A row has been written. This is only true if a row
                    // was actually drawn. (i.e. it had a height >0)
                    _state = TableWriterState.RowsWritten;
                }
            }
        }

        /// <summary>
        /// Starts the table This will allow listeners to include a table header.
        /// </summary>
        private void StartTable()
        {
            // Raise the HeaderPaint event.
            OnHeaderPaint();

            // The first cell starts at the top-left hand corner.
            _cell = new Rectangle(_bounds.Left, _bounds.Top, 0, 0);
        }

        /// <summary>
        /// Writes cells to the page.
        /// </summary>
        /// <param name="cells">The cells to write.</param>
        /// <param name="hasPaintListener">Is there a listener to the PaintCell event?</param>
        private void WriteCells(List<Cell> cells, bool hasPaintListener)
        {
            // Go through the columns, and see if the corresponding cell in the row needs
            // drawing, or is obscured by a spanning cell on a previous row.
            int cellIndex = 0;
            int columnIndex = 0;
            while(cellIndex<cells.Count && columnIndex<_columns.Count)
            {
                // Is this slot already filled in? If not, draw the cell.
                if(0 == _extensions[columnIndex])
                {
                    // Get the cell and column information, and
                    // update the extension.
                    Cell cell = cells[cellIndex++];
                    _cell.Width = _columns[columnIndex].Width.Value;
                    _extensions[columnIndex]+=cell.MergeInformation.Rows;

                    // If the cell spans multiple columns then we need to make
                    // it wide enough.
                    for(int columnSpan = cell.MergeInformation.Columns-1; columnSpan>0; columnSpan--)
                        _cell.Width+=_columns[++columnIndex].Width.Value;

                    // If the cell spans multiple rows then we need to make it
                    // high enough.
                    // TODO: This should use merge information?
                    foreach(Row r in _rowsToDraw) _cell.Height+=r.Height.Value;

                    // If there is a listener then we don't paint the cells ourselves.
                    if (false == hasPaintListener)
                    {
                        // Paint the background and draw the foreground.
                        if (null != cell.Background)
                            _g.FillRectangle(cell.Background, _cell);

                        // We take the cell padding into account.
                        Rectangle content = _cell;
                        content.Expand(-_tableOptions.Padding * 2, -_tableOptions.Padding * 2);

                        if (null != cell.Content)
                            _g.DrawStyledText(cell.Content, content, cell.Format ?? _defaultFormat);
                    }
                    else OnCellPaint(cell);

                    // Next cell
                    _cell.Move(_cell.Width, 0);
                    ++columnIndex;
                }

                // Reduce the extension by one.
                _extensions[columnIndex]--;
            }
        }
        #endregion



        // Protected implementation
        // ========================
        #region Protected implementation
        /// <summary>
        /// Raises the BorderPaint event.
        /// </summary>
        protected void OnBorderPaint()
        {
            EventArgs args = EventArgs.Empty;

            if (null != BorderPaint) BorderPaint(this, args);
        }

        /// <summary>
        /// Raises the CellPaint event.
        /// </summary>
        /// <param name="cell">The cell to be painted.</param>
        protected void OnCellPaint(Cell cell)
        {
            EventArgs args = EventArgs.Empty;

            if (null != CellPaint) CellPaint(this, args);
        }

        /// <summary>
        /// Raises the FooterPaint event.
        /// </summary>
        protected void OnFooterPaint()
        {
            EventArgs args = EventArgs.Empty;

            if (null != FooterPaint) FooterPaint(this, args);
        }

        /// <summary>
        /// Raises the HeaderPaint event.
        /// </summary>
        protected void OnHeaderPaint()
        {
            EventArgs args = EventArgs.Empty;

            if (null != HeaderPaint) HeaderPaint(this, args);
        }
        #endregion



        // Public constructor
        // ==================
        #region Public constructor
        /// <summary>
        /// Constructs a new TableWriter instance using the specified Graphics
        /// instance. The TableWriter will output the table primitives onto that
        /// Graphics instance.
        /// </summary>
        /// <param name="graphics">The Graphics instance.</param>
        internal TableWriter(Graphics graphics)
        {
            System.Diagnostics.Debug.Assert(null != graphics);

            _defaultFormat = new StringFormat()
            {
                HorizontalAlignment = HorizontalAlignment.Center,
                VerticalAlignment = VerticalAlignment.Center
            };

            _g = graphics;
            _rowsToDraw = new Queue<Row>();
            _state = TableWriterState.Closed;
        }
        #endregion



        // Private types
        // =============
        #region TableWriterState
        /// <summary>
        /// The TableWriterState enumeration lists the possible states for the table writer. The current state
        /// determines the validity of table operations, and controls header and footer placement.
        /// </summary>
        private enum TableWriterState
        {
            /// <summary>
            /// A table has not been opened. This is the initial state of a TableWriter instance.
            /// </summary>
            Closed,

            /// <summary>
            /// A table has been opened, but no rows written to it on the current page.
            /// </summary>
            Opened,

            /// <summary>
            /// A table has been opened and at least one row has been written to the current page.
            /// </summary>
            RowsWritten
        }
        #endregion

    } // TableWriter class
}
